#import "ThumbnailView.h"

#import "Controller.h"
#import "FolderModel.h"
#import "KeyCodes.h"
#import "PreferencesData.h"
#import "ThumbData.h"
#import "Utility.h"


#define GAP_WIDTH			4
#define GAP_HEIGHT			2
#define SELECTION_WIDTH		3
#define SELECTION_HEIGHT	3
#define TS_RADIUS			4


NSString *TVCurrentIndexChangedNotification = @"TVCurrentIndexChangedNotification";


static NSImage *movieIcon = nil;

@implementation ThumbnailView

//------------------------------------------------------------------------------
- (id) initWithFrame: (NSRect) frameRect
{
	self = [super initWithFrame: frameRect];
	if (nil != self)
	{
		acceptsFirstResponder = YES;
		calculatingSize = NO;
		isVertical = YES;
		isAStrip = NO;
		currentIndex = -1;
		showName = YES;
		showDate = YES;
		showThumbState = [[[Controller sharedController] preferencesData] markGeneratedThumbnails];

		dateFormatter = [[NSDateFormatter alloc] initWithDateFormat
				: @"%m/%d/%y %I:%M %p"  
				allowNaturalLanguage: NO];

		[self setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable];

		folderModel = nil;

		shadow = [[NSShadow alloc] init];
		[shadow setShadowBlurRadius: 4];

		maxTNWidth = 0;
		maxTNHeight = 0;
		firstShiftSelection = -1;

		nameAttrs = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
			[NSColor blackColor], NSForegroundColorAttributeName,
			nil, //[NSColor whiteColor], NSBackgroundColorAttributeName,
			nil];
		textHeight = [@"W" sizeWithAttributes: nameAttrs].height;

		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (frameChanged:)
			name: NSViewFrameDidChangeNotification
			object: [self enclosingScrollView]];

		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (prefsDataChanged:)
			name: PBPrefencesDidChangeNotification
			object: nil];

		[self calcAndSetSize];

		[self setMenu: [[Controller sharedController] imageMenu]];


		if (nil == movieIcon)
		{
			NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: @"movieicon.tiff"];
			movieIcon = [[NSImage alloc] initWithContentsOfFile: path];
		}
	}

	return self;
}

//------------------------------------------------------------------------------
- (BOOL) showDate
{
	return showDate;
}

//------------------------------------------------------------------------------
- (BOOL) showName
{
	return showName;
}

//------------------------------------------------------------------------------
- (void) setShowDate:(BOOL) newShowDate
{
	showDate = newShowDate;
	[self calcAndSetSize];
}

//------------------------------------------------------------------------------
- (void) setShowName:(BOOL) newShowName
{
	showName = newShowName;
	[self calcAndSetSize];
}

//------------------------------------------------------------------------------
- (void) setForegroundColor:(NSColor*) newColor
{
	if (nil == newColor)
	{
		[nameAttrs removeObjectForKey: NSForegroundColorAttributeName];
	}
	else
	{
		[nameAttrs setObject: newColor  forKey: NSForegroundColorAttributeName];
	}
}

//------------------------------------------------------------------------------
- (void) dealloc
{
	[dateFormatter release];
	[FolderModel releaseInstance: folderModel];
	[shadow release];
	[nameAttrs release];

	[[NSNotificationCenter defaultCenter] removeObserver: self];

	[super dealloc];
}

//------------------------------------------------------------------------------
- (unsigned int) draggingSourceOperationMaskForLocal:(BOOL) isLocal
{
	return NSDragOperationCopy | NSDragOperationMove;
}

//------------------------------------------------------------------------------
- (NSDragOperation) draggingEntered:(id <NSDraggingInfo>) sender
{
	return [[Controller sharedController] draggingEnteredHelper: sender];
}

//------------------------------------------------------------------------------
- (BOOL) prepareForDragOperation:(id <NSDraggingInfo>) sender
{
	return [[Controller sharedController] prepareForDragOperationHelper: sender];
}

//------------------------------------------------------------------------------
- (BOOL) performDragOperation:(id <NSDraggingInfo>) sender
{
	return [[Controller sharedController] performDragOperationHelper: sender];
}

//------------------------------------------------------------------------------
- (void) mouseDragged:(NSEvent *) evt
{
	NSArray *selItems = [self selectedItems];
	int selCount = [selItems count];
	if (0 == selCount)
	{
		return;
	}

	// Have we moved far enough since the mouse down to initiate a move?
	NSPoint pt = [evt locationInWindow];
	if (abs(ptLastMouseDown.x - pt.x) < 5 &&
		abs(ptLastMouseDown.y - pt.y) < 5)
	{
		return;
	}

	NSMutableArray *fileList = [NSMutableArray arrayWithCapacity: selCount];
	int idx;
	for (idx = 0; idx < selCount; ++idx)
	{
		[fileList addObject: [[selItems objectAtIndex: idx] fullPath]];
	}

	NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSDragPboard];
	[pb declareTypes: [NSArray arrayWithObject: NSFilenamesPboardType] owner: nil];
	[pb setPropertyList: fileList  forType: NSFilenamesPboardType];

	NSImage *dragImage = [[NSWorkspace sharedWorkspace] iconForFiles: fileList];

	NSPoint dragPosition = [self convertPoint:[evt locationInWindow] fromView:nil];
	dragPosition.x -= [dragImage size].height / 2;
	dragPosition.y -= [dragImage size].width / 2;

	[self dragImage
		: dragImage 
		at: dragPosition
		offset: NSZeroSize
		event: evt
		pasteboard: pb
		source: self
		slideBack: YES];
}

//------------------------------------------------------------------------------
- (void) rightMouseDown: (NSEvent *) evt
{
	// Selection-wise, have the right mouse button do the same thing as the
	// left mouse button (like iTunes does). But also allow the default thing
	// to happen, so our context menu can show up.
	[self mouseDown: evt];
	[super rightMouseDown: evt];
}

//------------------------------------------------------------------------------
- (void) mouseDown: (NSEvent *) evt
{
	extendedSelection = false;

	[[self window] makeFirstResponder: self];


	int clickCount = [evt clickCount];
	unsigned int flags = [evt modifierFlags];
	NSPoint pt = [evt locationInWindow];
	ptLastMouseDown = pt;
	pt = [self convertPoint: pt  fromView: nil];
	int index = [self indexFromPoint: pt];
	if (-1 == index)
	{
		return;
	}

	BOOL shiftDown = 0 != (flags & NSShiftKeyMask);
	BOOL commandDown = 0 != (flags & NSCommandKeyMask);
	if (commandDown)
	{
		shiftDown = NO;
	}

	extendedSelection = shiftDown | commandDown;

	int oldIndex = currentIndex;
	currentIndex = index;
	if (currentIndex != oldIndex)
	{
		[[NSNotificationCenter defaultCenter] postNotificationName 
			: TVCurrentIndexChangedNotification
			object: self 
			userInfo: nil];
	}


	// Clicking on an already selected item does NOT clear the selection (and
	// may be the start of a drag operation).
	if (NO == commandDown && NO == shiftDown &&
		index >= 0 && 1 == clickCount && [self isSelected: index])
	{
		return;
	}

	if (NO == shiftDown && NO == commandDown)
	{
		firstShiftSelection = -1;
		[self clearSelection];
		[self setNeedsDisplayInRect: [[self enclosingScrollView] documentVisibleRect]];
	}

	if (index < 0)
	{
		return;
	}

	NSRect rect = [self rectFromIndex: index];
	NSBezierPath *path = [self getRoundedRect: rect.origin];
	if ([path containsPoint: pt])
	{
		if (-1 == firstShiftSelection && NO == commandDown)
		{
			firstShiftSelection = index;
		}
		else
		if (YES == shiftDown)
		{
			[self clearSelection];
			int idx;
			int start = firstShiftSelection;
			int end = index;

			if (start > end)
			{
				start = index;
				end = firstShiftSelection;
			}

			for (idx = start; idx <= end; ++idx)
			{
				[self selectItem: idx  markSelected: YES];
			}

			[self setNeedsDisplayInRect: [[self enclosingScrollView] documentVisibleRect]];
			return;
		}


		[self selectItem:index  markSelected:![self isSelected: index]];
	}

	if (clickCount > 1)
	{
		// Notify our delegate when an item is double clicked
		if ([delegate respondsToSelector
				:@selector(thumbnailView: doubleClicked:)])
		{
			ThumbData *td = [folderModel tdAtIndex: index];
			[delegate thumbnailView: self  doubleClicked: [td fullPath]];
		}
	}
}

//------------------------------------------------------------------------------
// NOT called once a drag operation has started. Used to clear selection of
// everything but the current item if a single item is clicked on.
- (void) mouseUp: (NSEvent *) evt
{
	if (YES == extendedSelection)
	{
		return;
	}

	NSPoint ptDown = [self convertPoint: ptLastMouseDown  fromView: nil];
	int indexDown = [self indexFromPoint: ptDown];

	NSPoint ptUp = [self convertPoint: [evt locationInWindow]  fromView: nil];
	int indexUp = [self indexFromPoint: ptUp];

	if  (indexDown == indexUp && [self selectedCount] > 1)
	{
		[self clearSelection];
		[self selectItem:indexDown  markSelected:YES];
		[self setNeedsDisplayInRect: [[self enclosingScrollView] documentVisibleRect]];
	}
}

//------------------------------------------------------------------------------
- (BOOL) vertical
{
	return isVertical;
}

//------------------------------------------------------------------------------
/**
 * Caller is responsible for forcing a re-display
 */
- (void) setVertical:(BOOL)vertical
{
	isVertical = vertical;
	[self calcAndSetSize];
}

//------------------------------------------------------------------------------
- (BOOL) isAStrip
{
	return isAStrip;
}

//------------------------------------------------------------------------------
- (void) setIsAStrip:(BOOL) strip
{
	isAStrip = strip;
	[self calcAndSetSize];
}

//------------------------------------------------------------------------------
- (void) calcAndSetSize
{
	// Our notification occurs when the scroll view changes its size - and that
	// is changed when we set our frame size, below. In an attempt at minimizing
	// the thrashing, we will only calculate the size if we aren't already.
	if (YES == calculatingSize)
	{
		return;
	}
	calculatingSize = YES;


	unsigned count = 0;
	if (nil != folderModel)
	{
		count = [folderModel count];
	}

	int width = [[self enclosingScrollView] contentSize].width;
	int height = [[self enclosingScrollView] contentSize].height;

	if (0 != count && 0 != totalItemWidth && 0 != totalItemHeight)
	{
		if (YES == isVertical)
		{
			int cols = width / totalItemWidth;
			if (cols < 1)
			{
				cols = 1;
			}

			int rows = count / cols + (0 != count % cols);

			int viewHeight = rows * totalItemHeight;
			if (YES == isAStrip)
			{
				viewHeight += height - totalItemHeight;
			}

			if (viewHeight > height)
			{
				height = viewHeight;
			}
		}
		else
		{
			int rows = height / totalItemHeight;
			if (rows < 1)
			{
				rows = 1;
			}

			int cols = count / rows + (0 != count % rows);

			int viewWidth = cols * totalItemWidth;
			if (YES == isAStrip)
			{
				viewWidth += width - totalItemWidth;
			}

			if (viewWidth > width)
			{
				width = viewWidth;
			}
		}
	}

	NSSize newSize = NSMakeSize(width, height);
	NSSize szCurrent = [self frame].size;
	if (NO == NSEqualSizes(newSize, szCurrent))
	{
		[self setFrameSize: newSize];
		[self setNeedsDisplay: YES];
	}


	calculatingSize = NO;
}

//------------------------------------------------------------------------------
- (void) frameChanged: (NSNotification *) notification
{
	if (self != [notification object])
	{
		[self calcAndSetSize];
	}
}

//------------------------------------------------------------------------------
- (void) prefsDataChanged: (NSNotification *) note
{
	showThumbState = [[[Controller sharedController] preferencesData] markGeneratedThumbnails];
	[super display];
}

//------------------------------------------------------------------------------
- (void) clearThumbnails
{
	if (nil != folderModel)
	{
		[[NSNotificationCenter defaultCenter] removeObserver :self  name:nil  object:folderModel];
		[FolderModel releaseInstance :folderModel];
		folderModel = nil;
	}

	[self calcAndSetSize];
	[self display];
}

//------------------------------------------------------------------------------
- (FolderModel*) folderModel
{
	return folderModel;
}

//------------------------------------------------------------------------------
- (void) setFolderModel:(FolderModel*) fm  addThumbs:(BOOL) addThumbs
{
	if (nil != selectedItems)
	{
		free(selectedItems);
		selectedItems = nil;
	}

	if (nil != folderModel)
	{
		[[NSNotificationCenter defaultCenter] removeObserver :self  name:nil  object:folderModel];
		[FolderModel releaseInstance :folderModel];
		folderModel = nil;
	}

	if (nil != fm)
	{
		folderModel = [FolderModel instance: [fm path]  sort:[fm sortedBy]  stop:nil];
		selectedItems = calloc([folderModel count], sizeof (BOOL));
	}

	maxTNWidth = 0;
	maxTNHeight = 0;
	totalItemWidth = 0;
	totalItemHeight = 0;
	currentIndex = -1;

	[self calcAndSetSize];
	[self scrollPoint: NSMakePoint(0, NSHeight([self frame]))];


	if (nil != folderModel)
	{
		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (folderModelItemNotification:)
			name: FMItemsAddedNotification
			object: folderModel];

		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (folderModelItemNotification:)
			name: FMItemsRemovedNotification
			object: folderModel];

		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (folderModelItemNotification:)
			name: FMItemsUpdatedNotification
			object: folderModel];

		[[NSNotificationCenter defaultCenter] addObserver
			: self
			selector: @selector (folderModelThumbAvailableNotification:)
			name: FMThumbAvailableNotification
			object: folderModel];
	}


	// Process any thumbnails in the list
	if (YES == addThumbs || 0 == [folderModel queueCount])
	{
		int count = [folderModel count];
		int idx;
		for (idx = 0; idx < count; ++idx)
		{
			[self thumbnailAvailable: [folderModel tdAtIndex: idx]];
		}
	}

	[self display];
}

//------------------------------------------------------------------------------
- (void) resetCurrentIndex:(id) nada
{
	int index = currentIndex;
	currentIndex = -1;
	[self setCurrent: index];
}

//------------------------------------------------------------------------------
- (void) thumbnailAvailable:(ThumbData*) thumbData
{
	BOOL sizeChanged = NO;
	if (nil != [thumbData thumbnail])
	{
		if ([thumbData width] > maxTNWidth)
		{
			maxTNWidth = [thumbData width];
			sizeChanged = YES;
		}

		if ([thumbData height] > maxTNHeight)
		{
			maxTNHeight = [thumbData height];
			sizeChanged = YES;
		}
	}

	if (YES == sizeChanged)
	{
		BOOL resetCurrent = 0 == totalItemWidth + totalItemHeight && -1 != currentIndex;

		totalItemWidth = maxTNWidth + 2 * SELECTION_WIDTH + 2 * GAP_WIDTH;
		totalItemHeight = maxTNHeight + 2 * SELECTION_HEIGHT + 2 * GAP_HEIGHT;
		if (showName)
		{
			totalItemHeight += textHeight;
		}
		if (showDate)
		{
			totalItemHeight += textHeight;
		}

		[self calcAndSetSize];
		[self scrollPoint: NSMakePoint(0, NSHeight([self frame]))];
		[self setNeedsDisplay: YES];

		if (YES == resetCurrent)
		{
			[self performSelectorOnMainThread
					:@selector(resetCurrentIndex:)  
					withObject:nil  
					waitUntilDone:NO];
		}
	}

	// Paint this thumbnail if it's visible
	int index = [self indexFromThumbData: thumbData];
	NSRect visibleRect = [(NSClipView*)[self superview] documentVisibleRect];
	NSRect rect = [self rectFromIndex: index];
	if (YES == sizeChanged || NSIntersectsRect(visibleRect, rect))
	{
		[self setNeedsDisplayInRect: visibleRect];
	}
}

//------------------------------------------------------------------------------
- (void) folderModelItemNotification:(NSNotification *) note
{
	if (folderModel != [note object])
	{
NSLog(
	@"folderModelItemNotification notification for wrong model (have %@ vs %@)", 
	[folderModel path], 
	[[note object] path]);

		return;
	}

	// Adding or removing items clears selection (and resizes the selection array
	// to the proper size).
	if (FMItemsRemovedNotification == [note name] ||
		FMItemsAddedNotification == [note name])
	{
		free(selectedItems);
		selectedItems = calloc([folderModel count], sizeof (BOOL));

		if (currentIndex >= [folderModel count])
		{
			currentIndex = [folderModel count] - 1;
		}
		[self resetCurrentIndex: nil];
	}

	[self calcAndSetSize];
	[super display];
}

//------------------------------------------------------------------------------
- (void) folderModelThumbAvailableNotification:(NSNotification*) note
{
	if (folderModel != [note object])
	{
NSLog(
	@"folderModelThumbAvailableNotification notification for wrong model (have %@ vs %@)", 
	[folderModel path], 
	[[note object] path]);

		return;
	}

	ThumbData *td = [[note userInfo] objectForKey: FM_THUMB_DATA];
	if (nil != td)
	{
		[self thumbnailAvailable: td];
	}
}

//------------------------------------------------------------------------------
- (void) drawMark:(int)x  y:(int)y  outerColor:(NSColor*)outerColor
{
	NSBezierPath* path = [NSBezierPath bezierPath];
	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(x, y) 
		radius: TS_RADIUS
		startAngle: 0.0 
		endAngle: 360.0];
	[path closePath];
	[outerColor set];
	[path fill];

	path = [NSBezierPath bezierPath];
	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(x, y) 
		radius: TS_RADIUS / 2
		startAngle: 0.0 
		endAngle: 360.0];
	[path closePath];
	[[NSColor whiteColor] set];
	[path fill];
}

//------------------------------------------------------------------------------
- (void) drawRect: (NSRect) rect
{
	[super drawRect: rect];

	int count = [folderModel count];
	if (count > 0 && totalItemWidth > 0 && totalItemHeight)
	{
		int startIndex = [self indexFromPoint
				: NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)];
		int endIndex = [self indexFromPoint
				: NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y)];

		if (startIndex < 0)
		{
			startIndex = 0;
		}

		if (endIndex >= count || -1 == endIndex)
		{
			endIndex = count - 1;
		}

   		NSRect viewBounds = [self bounds];
		int columns = NSWidth(viewBounds) / totalItemWidth;
		if (columns < 1)
		{
			columns = 1;
		}

		NSColor *shadowColor = [shadow shadowColor];
		[shadow setShadowColor: nil];
		[shadow set];

		int viewHeight = NSHeight(viewBounds);
		int viewWidth = NSWidth(viewBounds);

		int idx;
		for (idx = startIndex; idx <= endIndex; ++idx)
		{
			int row = idx / columns;
			int col = idx % columns;
//NSLog(@"columns: %d; index - %d; row - %d; col - %d", columns, idx, row, col);
			ThumbData *td = [folderModel tdAtIndex: idx];
			NSImage *thumbnail = [td thumbnail];

			NSPoint pt = NSMakePoint(
				col * totalItemWidth,
				viewHeight - (row * totalItemHeight) - totalItemHeight);

			// Center the row/column in the available view
			if (YES == isAStrip)
			{
				if (isVertical)
				{
					// We're one column wide - center by width
					if (totalItemWidth < viewWidth)
					{
						pt.x = (viewWidth - totalItemWidth) / 2;
					}

					// And offset the images by half the window height
					pt.y -= ([[self enclosingScrollView] contentSize].height - totalItemHeight) / 2;
				}
				else
				{
					// We're one row wide - center by height
					if (totalItemHeight < viewHeight)
					{
						pt.y = (viewHeight - totalItemHeight) / 2;
					}

					// And offset the images by half the window width
					pt.x += ([[self enclosingScrollView] contentSize].width - totalItemWidth) / 2;
				}
			}

//NSLog(@"rect for %u is %@", idx, NSStringFromRect([self rectFromIndex: idx]));
//NSLog(@"height = %d; point for %d x %d is %@", scrollHeight, row, col, NSStringFromPoint(pt));

			if ([self isSelected: idx])
			{
				NSBezierPath *path = [self getRoundedRect: pt];
				[path fill];
//NSLog(@"index %d is row %d - col %d (%@)", idx, row, col, NSStringFromRect(selRect));
			}


			int xOff = pt.x + GAP_WIDTH + SELECTION_WIDTH;
			int yOff = pt.y + GAP_HEIGHT + SELECTION_HEIGHT;
			if (showName)
			{
				yOff += textHeight;
			}
			if (showDate)
			{
				yOff += textHeight;
			}

			int thumbWidth = [td width];
			int thumbHeight = [td height];
			if (0 == thumbWidth || 0 == thumbHeight)
			{
				thumbWidth = maxTNWidth;
				thumbHeight = maxTNHeight;
			}

			if (thumbWidth < maxTNWidth)
			{
				xOff += (maxTNWidth - thumbWidth) / 2;
			}
			if (thumbHeight < maxTNHeight)
			{
				yOff += (maxTNHeight - thumbHeight) / 2;
			}

			[shadow setShadowColor: shadowColor];
			[shadow set];
			[thumbnail 
				drawAtPoint: NSMakePoint(xOff, yOff)
				fromRect: NSMakeRect(0, 0, [td width], [td height])
				operation: NSCompositeCopy
				fraction: 1.0];

			[shadow setShadowColor: nil];
			[shadow set];

			int thumbState = [td thumbState];
			if (showThumbState && 
				FILE_TYPE_JPEG == [td fileType] &&
				THUMB_STATE_VALID_EXIF != thumbState &&
				THUMB_STATE_UNKNOWN != thumbState)
			{
				int y = yOff + TS_RADIUS;
				int x = xOff + thumbWidth - TS_RADIUS;
				[self drawMark :x  y:y  outerColor:[NSColor alternateSelectedControlColor]];
			}

			if (MEDIA_TYPE_MOVIE == [td mediaType])
			{
				NSSize size = [movieIcon size];
				[movieIcon drawAtPoint
						: NSMakePoint(xOff, yOff)
						fromRect: NSMakeRect(0, 0, size.width, size.height)
						operation: NSCompositeSourceAtop
						fraction: 1.0];
			}

			if (showName)
			{
				yOff -= textHeight;
				[[td name] drawAtPoint: NSMakePoint(xOff, yOff) withAttributes: nameAttrs];
			}

			NSString *date = [dateFormatter stringForObjectValue: [td date]];
			if (showDate)
			{
				yOff -= textHeight;
				[date drawAtPoint: NSMakePoint(xOff, yOff) withAttributes: nameAttrs];
			}

			if (showThumbState && NO == [td doDatesMatch])
			{
				NSSize sizeDate = [date sizeWithAttributes: nameAttrs];
				int x = xOff + sizeDate.width + 7;
				int y = yOff + (textHeight / 2);
				[self drawMark:x  y:y  outerColor:[NSColor redColor]];
			}
		}

		[shadow setShadowColor: shadowColor];
	}
}

//------------------------------------------------------------------------------
- (NSBezierPath*) getRoundedRect: (NSPoint) pt
{
	NSRect aRect = NSMakeRect(
		pt.x + GAP_WIDTH,
		pt.y + GAP_HEIGHT,
		totalItemWidth - 2 * GAP_WIDTH,
		totalItemHeight - 2 * GAP_HEIGHT);

	NSBezierPath* path = [NSBezierPath bezierPath];
	float radius = 10;
	radius = MIN(radius, 0.5f * MIN(NSWidth(aRect), NSHeight(aRect)));
	NSRect rect = NSInsetRect(aRect, radius, radius);
	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(NSMinX(rect), NSMinY(rect)) 
		radius: radius 
		startAngle: 180.0 
		endAngle: 270.0];

	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(NSMaxX(rect), NSMinY(rect)) 
		radius: radius 
		startAngle: 270.0 
		endAngle: 360.0];

	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(NSMaxX(rect), NSMaxY(rect)) 
		radius: radius 
		startAngle: 0.0 
		endAngle: 90.0];

	[path appendBezierPathWithArcWithCenter
		: NSMakePoint(NSMinX(rect), NSMaxY(rect)) 
		radius: radius 
		startAngle: 90.0 
		endAngle: 180.0];
	[path closePath];
	[[NSColor alternateSelectedControlColor] set];

	return path;
}

//------------------------------------------------------------------------------
- (NSRect) rectFromIndex: (int) index
{
	NSRect viewBounds = [self bounds];
	int columns = NSWidth(viewBounds) / totalItemWidth;
	if (columns < 1)
	{
		columns = 1;
	}

	int row = index / columns;
	int col = index % columns;

	NSRect rect = NSMakeRect(
		col * totalItemWidth,
		NSHeight(viewBounds) - (row * totalItemHeight) - totalItemHeight + 1,
		totalItemWidth - 1,
		totalItemHeight - 1);

	// Center the row/column in the available view
	if (YES == isAStrip)
	{
		if (isVertical)
		{
			// We're one column wide - center by width
			if (totalItemWidth < NSWidth(viewBounds))
			{
				rect.origin.x = (NSWidth(viewBounds) - totalItemWidth) / 2;
			}

			rect.origin.y -= ([[self enclosingScrollView] contentSize].height - totalItemHeight) / 2;
		}
		else
		{
			// We're one row wide - center by height
			if (totalItemHeight < NSHeight(viewBounds))
			{
				rect.origin.y = (NSHeight(viewBounds) - totalItemHeight) / 2;
			}

			rect.origin.x += ([[self enclosingScrollView] contentSize].width - totalItemWidth) / 2;
		}
	}

	return rect;
}

//------------------------------------------------------------------------------
- (int) indexFromThumbData: (ThumbData *) thumbData
{
	int count = [folderModel count];
	int idx;
	for (idx = 0; idx < count; ++idx)
	{
		ThumbData *td = [folderModel tdAtIndex: idx];
		if (td == thumbData)
		{
			return idx;
		}
	}

	return -1;
}

//------------------------------------------------------------------------------
- (int) indexFromPoint: (NSPoint) point
{
	if (0 == [folderModel count])
	{
		return -1;
	}

	NSRect viewBounds = [self bounds];
	int columns = NSWidth(viewBounds) / totalItemWidth;
	if (columns < 1)
	{
		columns = 1;
	}

	// Center the row/column in the available view
	if (YES == isAStrip)
	{
		if (isVertical)
		{
			// We're one column wide - center by width
			if (totalItemWidth < NSWidth(viewBounds))
			{
				point.x -= (NSWidth(viewBounds) - totalItemWidth) / 2;
			}

			point.y += ([[self enclosingScrollView] contentSize].height - totalItemHeight) / 2;
		}
		else
		{
			// We're one row wide - center by height
			if (totalItemHeight < NSHeight(viewBounds))
			{
				point.y -= (NSHeight(viewBounds) - totalItemHeight) / 2;
			}

			point.x -= ([[self enclosingScrollView] contentSize].width - totalItemWidth) / 2;
		}
	}

	int col = point.x / totalItemWidth;
	int row = (NSHeight(viewBounds) - point.y) / totalItemHeight;

	int index = row * columns + col;
	if (index >= [folderModel count] || index < 0)
	{
		return -1;
	}

	return index;
}

//------------------------------------------------------------------------------
- (void) selectCurrent
{
	int count = [folderModel count];
	if (0 == count)
	{
		return;
	}

	if (currentIndex < 0)
	{
		currentIndex = 0;
	}
	else
	if (currentIndex >= count)
	{
		currentIndex = count - 1;
	}

	[self selectItem: currentIndex  markSelected: YES];
	NSRect rect = [self rectFromIndex: currentIndex];
	[self setNeedsDisplayInRect: rect];
}

//------------------------------------------------------------------------------
- (void) setCurrent:(int)index
{
	int count = [folderModel count];
	if (index < 0 || index >= count)
	{
		return;
	}


	if (currentIndex != index)
	{
		// Clear the current mark
		if (-1 != currentIndex)
		{
			[self selectItem: currentIndex  markSelected: NO];
		}

		currentIndex = index;
		[self selectItem: currentIndex  markSelected: YES];

		[[NSNotificationCenter defaultCenter] postNotificationName 
			: TVCurrentIndexChangedNotification
			object: self 
			userInfo: nil];
	}

	NSRect rect = [self rectFromIndex: currentIndex];
	if (isAStrip)
	{
		NSPoint pt = rect.origin;
		if (isVertical)
		{
			pt.y -= ([[self enclosingScrollView] contentSize].height - totalItemHeight) / 2;
		}
		else
		{
			pt.x -= ([[self enclosingScrollView] contentSize].width - totalItemWidth) / 2;
		}

		[self scrollPoint: pt];
	}
	else
	{
		[self scrollRectToVisible: rect];
	}
}

//------------------------------------------------------------------------------
- (int) currentIndex
{
	return currentIndex;
}

//------------------------------------------------------------------------------
- (BOOL) hasSelection
{
	int count = [folderModel count];
	if (nil == selectedItems || count <= 0)
	{
		return NO;
	}

	return NULL != memchr(selectedItems, YES, count);
}

//------------------------------------------------------------------------------
- (int) selectedCount
{
	int count = [folderModel count];
	int selCount = 0;
	if (nil == selectedItems || count <= 0)
	{
		return selCount;
	}

	int index = 0;
	void *offset = memchr(&(selectedItems[index]), YES, count - index);
	while (NULL != offset)
	{
		index = (offset - (void*)selectedItems) / sizeof (BOOL);
		++selCount;

		// Search again, just beyond this last found item
		offset = memchr(&(selectedItems[index + 1]), YES, count - index);
	}

	return selCount;
}

//------------------------------------------------------------------------------
- (NSArray *) selectedItems
{
	int count = [folderModel count];
	NSMutableArray *array = [NSMutableArray arrayWithCapacity: count];
	if (nil == selectedItems || count <= 0)
	{
		return array;
	}

	int index = 0;
	void *offset = memchr(&(selectedItems[index]), YES, count - index);
	while (NULL != offset)
	{
		index = (offset - (void*)selectedItems) / sizeof (BOOL);
		[array addObject: [folderModel tdAtIndex: index]];

		// Search again, just beyond this last found item
		offset = memchr(&(selectedItems[index + 1]), YES, count - index);
	}

	return array;
}

//------------------------------------------------------------------------------
- (void) selectAll
{
	int count = [folderModel count];

	if (nil != selectedItems && count > 0)
	{
		memset(selectedItems, 1, count * sizeof (BOOL));
	}

	[self setNeedsDisplayInRect: [[self enclosingScrollView] documentVisibleRect]];
}

//------------------------------------------------------------------------------
- (void) clearSelection
{
	int count = [folderModel count];

	if (nil != selectedItems && count > 0)
	{
		memset(selectedItems, 0, count * sizeof (BOOL));
	}
}

//------------------------------------------------------------------------------
- (BOOL) isSelected:(int) index
{
	if (nil == selectedItems || index >= [folderModel count] || index < 0)
	{
		return NO;
	}

	return selectedItems[index];
}

//------------------------------------------------------------------------------
- (void) selectItem:(int) index  markSelected:(BOOL) select
{
	if (nil == selectedItems || index >= [folderModel count] || index < 0)
	{
		return;
	}

	if (select == selectedItems[index])
	{
		return;
	}

	// Selection changed
	selectedItems[index] = select;
	NSRect rect = [self rectFromIndex: index];
	[self setNeedsDisplayInRect: rect];

	if ([delegate respondsToSelector:@selector(thumbnailView: selectionChanged: selected: item:)])
	{
		[delegate thumbnailView
			: self  
			selectionChanged: index
			selected: select
			item: [folderModel tdAtIndex: index]];
	}
}

//------------------------------------------------------------------------------
- (void) displayThumbData:(ThumbData*) td
{
	int index = [self indexFromThumbData: td];
	if (-1 != index)
	{
		[self setNeedsDisplayInRect: [self rectFromIndex: index]];
	}
}

//------------------------------------------------------------------------------
- (int) maxThumbWidth
{
	return maxTNWidth;
}

//------------------------------------------------------------------------------
- (int) maxThumbHeight
{
	return maxTNHeight;
}

//------------------------------------------------------------------------------
- (id) delegate
{
	return delegate;
}

//------------------------------------------------------------------------------
- (void) setDelegate: (id) newDelegate
{
	delegate = newDelegate;
}

//------------------------------------------------------------------------------
- (void) keyDown:(NSEvent *) evt
{
	int cols;
	int count;
	int newIndex = currentIndex;
	switch ([evt keyCode])
	{
		case VKC_HOME:
			newIndex = 0;
			break;
		case VKC_END:
			newIndex = count - 1;
			break;

		case VKC_UP_ARROW:
		case VKC_DOWN_ARROW:
		case VKC_LEFT_ARROW:
		case VKC_RIGHT_ARROW:
			count = [folderModel count];
			if (0 == count)
			{
				return;
			}

			if (-1 == newIndex)
			{
				newIndex = 0;
			}
			else
			{
				cols = [[self enclosingScrollView] contentSize].width / totalItemWidth;
				switch ([evt keyCode])
				{
					case VKC_UP_ARROW:
						if (currentIndex < cols)
						{
							return;
						}
						newIndex -= cols;
						break;

					case VKC_DOWN_ARROW:
						if (currentIndex + cols >= count)
						{
							return;
						}
						newIndex += cols;
						break;

					case VKC_LEFT_ARROW:
						newIndex -= 1;
						break;

					case VKC_RIGHT_ARROW:
						newIndex += 1;
						break;
				}
			}
			break;

		default:
			[super keyDown: evt];
			return;
	}

	// Ensure we stay in bounds
	if (newIndex < 0)
	{
		newIndex = 0;
	}
	else
	if (newIndex >= count)
	{
		newIndex = count - 1;
	}

	// Do nothing if the index hasn't moved
	if (newIndex == currentIndex)
	{
		return;
	}

	if ([self isSelected: currentIndex])
	{
		[self selectItem:currentIndex  markSelected:NO];
	}

	currentIndex = newIndex;
	[self selectItem:currentIndex  markSelected:YES];
	NSRect rect = [self rectFromIndex: currentIndex];
	[self scrollRectToVisible: rect];

	[[NSNotificationCenter defaultCenter] postNotificationName 
		: TVCurrentIndexChangedNotification
		object: self 
		userInfo: nil];
}

//------------------------------------------------------------------------------
- (BOOL) acceptsFirstResponder
{
	return acceptsFirstResponder;
}

//------------------------------------------------------------------------------
- (void) setAcceptsFirstResponder:(BOOL) accepts
{
	acceptsFirstResponder = accepts;
}

//------------------------------------------------------------------------------
- (BOOL) canBecomeKeyView
{
	return YES;
}

@end
